/***
 * ASM: a very small and fast Java bytecode manipulation framework
 * Copyright (c) 2000-2005 INRIA, France Telecom
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holders nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */
package salve.asmlib;

import java.util.ArrayList;
import java.util.HashMap;

/**
 * A {@link salve.asmlib.MethodAdapter} to insert before, after and around
 * advices in methods and constructors.
 * <p>
 * The behavior for constructors is like this:
 * <ol>
 * 
 * <li>as long as the INVOKESPECIAL for the object initialization has not been
 * reached, every bytecode instruction is dispatched in the ctor code visitor</li>
 * 
 * <li>when this one is reached, it is only added in the ctor code visitor and
 * a JP invoke is added</li>
 * 
 * <li>after that, only the other code visitor receives the instructions</li>
 * 
 * </ol>
 * 
 * @author Eugene Kuleshov
 * @author Eric Bruneton
 */
@SuppressWarnings("unchecked")
public abstract class AdviceAdapter extends GeneratorAdapter implements Opcodes {
	private static final Object THIS = new Object();
	private static final Object OTHER = new Object();

	protected int methodAccess;
	protected String methodDesc;

	private boolean constructor;
	private boolean superInitialized;
	private ArrayList stackFrame;
	private HashMap branches;

	/**
	 * Creates a new {@link AdviceAdapter}.
	 * 
	 * @param mv
	 *            the method visitor to which this adapter delegates calls.
	 * @param access
	 *            the method's access flags (see {@link Opcodes}).
	 * @param name
	 *            the method's name.
	 * @param desc
	 *            the method's descriptor (see {@link Type Type}).
	 */
	public AdviceAdapter(final MethodVisitor mv, final int access, final String name, final String desc) {
		super(mv, access, name, desc);
		methodAccess = access;
		methodDesc = desc;

		constructor = "<init>".equals(name);
	}

	@Override
	public void visitCode() {
		mv.visitCode();
		if (!constructor) {
			superInitialized = true;
			onMethodEnter();
		} else {
			stackFrame = new ArrayList();
			branches = new HashMap();
		}
	}

	@Override
	public void visitFieldInsn(final int opcode, final String owner, final String name, final String desc) {
		mv.visitFieldInsn(opcode, owner, name, desc);

		if (constructor) {
			char c = desc.charAt(0);
			boolean longOrDouble = c == 'J' || c == 'D';
			switch (opcode) {
				case GETSTATIC:
					pushValue(OTHER);
					if (longOrDouble) {
						pushValue(OTHER);
					}
					break;
				case PUTSTATIC:
					popValue();
					if (longOrDouble) {
						popValue();
					}
					break;
				case PUTFIELD:
					popValue();
					if (longOrDouble) {
						popValue();
						popValue();
					}
					break;
				// case GETFIELD:
				default:
					if (longOrDouble) {
						pushValue(OTHER);
					}
			}
		}
	}

	@Override
	public void visitInsn(final int opcode) {
		if (constructor) {
			switch (opcode) {
				case RETURN: // empty stack
					onMethodExit(opcode);
					break;

				case IRETURN: // 1 before n/a after
				case FRETURN: // 1 before n/a after
				case ARETURN: // 1 before n/a after
				case ATHROW: // 1 before n/a after
					popValue();
					popValue();
					onMethodExit(opcode);
					break;

				case LRETURN: // 2 before n/a after
				case DRETURN: // 2 before n/a after
					popValue();
					popValue();
					onMethodExit(opcode);
					break;

				case NOP:
				case LALOAD: // remove 2 add 2
				case DALOAD: // remove 2 add 2
				case LNEG:
				case DNEG:
				case FNEG:
				case INEG:
				case L2D:
				case D2L:
				case F2I:
				case I2B:
				case I2C:
				case I2S:
				case I2F:
				case Opcodes.ARRAYLENGTH:
					break;

				case ACONST_NULL:
				case ICONST_M1:
				case ICONST_0:
				case ICONST_1:
				case ICONST_2:
				case ICONST_3:
				case ICONST_4:
				case ICONST_5:
				case FCONST_0:
				case FCONST_1:
				case FCONST_2:
				case F2L: // 1 before 2 after
				case F2D:
				case I2L:
				case I2D:
					pushValue(OTHER);
					break;

				case LCONST_0:
				case LCONST_1:
				case DCONST_0:
				case DCONST_1:
					pushValue(OTHER);
					pushValue(OTHER);
					break;

				case IALOAD: // remove 2 add 1
				case FALOAD: // remove 2 add 1
				case AALOAD: // remove 2 add 1
				case BALOAD: // remove 2 add 1
				case CALOAD: // remove 2 add 1
				case SALOAD: // remove 2 add 1
				case POP:
				case IADD:
				case FADD:
				case ISUB:
				case LSHL: // 3 before 2 after
				case LSHR: // 3 before 2 after
				case LUSHR: // 3 before 2 after
				case L2I: // 2 before 1 after
				case L2F: // 2 before 1 after
				case D2I: // 2 before 1 after
				case D2F: // 2 before 1 after
				case FSUB:
				case FMUL:
				case FDIV:
				case FREM:
				case FCMPL: // 2 before 1 after
				case FCMPG: // 2 before 1 after
				case IMUL:
				case IDIV:
				case IREM:
				case ISHL:
				case ISHR:
				case IUSHR:
				case IAND:
				case IOR:
				case IXOR:
				case MONITORENTER:
				case MONITOREXIT:
					popValue();
					break;

				case POP2:
				case LSUB:
				case LMUL:
				case LDIV:
				case LREM:
				case LADD:
				case LAND:
				case LOR:
				case LXOR:
				case DADD:
				case DMUL:
				case DSUB:
				case DDIV:
				case DREM:
					popValue();
					popValue();
					break;

				case IASTORE:
				case FASTORE:
				case AASTORE:
				case BASTORE:
				case CASTORE:
				case SASTORE:
				case LCMP: // 4 before 1 after
				case DCMPL:
				case DCMPG:
					popValue();
					popValue();
					popValue();
					break;

				case LASTORE:
				case DASTORE:
					popValue();
					popValue();
					popValue();
					popValue();
					break;

				case DUP:
					pushValue(peekValue());
					break;

				case DUP_X1:
					// TODO optimize this
				{
					Object o1 = popValue();
					Object o2 = popValue();
					pushValue(o1);
					pushValue(o2);
					pushValue(o1);
				}
					break;

				case DUP_X2:
					// TODO optimize this
				{
					Object o1 = popValue();
					Object o2 = popValue();
					Object o3 = popValue();
					pushValue(o1);
					pushValue(o3);
					pushValue(o2);
					pushValue(o1);
				}
					break;

				case DUP2:
					// TODO optimize this
				{
					Object o1 = popValue();
					Object o2 = popValue();
					pushValue(o2);
					pushValue(o1);
					pushValue(o2);
					pushValue(o1);
				}
					break;

				case DUP2_X1:
					// TODO optimize this
				{
					Object o1 = popValue();
					Object o2 = popValue();
					Object o3 = popValue();
					pushValue(o2);
					pushValue(o1);
					pushValue(o3);
					pushValue(o2);
					pushValue(o1);
				}
					break;

				case DUP2_X2:
					// TODO optimize this
				{
					Object o1 = popValue();
					Object o2 = popValue();
					Object o3 = popValue();
					Object o4 = popValue();
					pushValue(o2);
					pushValue(o1);
					pushValue(o4);
					pushValue(o3);
					pushValue(o2);
					pushValue(o1);
				}
					break;

				case SWAP: {
					Object o1 = popValue();
					Object o2 = popValue();
					pushValue(o1);
					pushValue(o2);
				}
					break;
			}
		} else {
			switch (opcode) {
				case RETURN:
				case IRETURN:
				case FRETURN:
				case ARETURN:
				case LRETURN:
				case DRETURN:
				case ATHROW:
					onMethodExit(opcode);
					break;
			}
		}
		mv.visitInsn(opcode);
	}

	@Override
	public void visitIntInsn(final int opcode, final int operand) {
		mv.visitIntInsn(opcode, operand);

		if (constructor && opcode != NEWARRAY) {
			pushValue(OTHER);
		}
	}

	@Override
	public void visitJumpInsn(final int opcode, final Label label) {
		mv.visitJumpInsn(opcode, label);

		if (constructor) {
			switch (opcode) {
				case IFEQ:
				case IFNE:
				case IFLT:
				case IFGE:
				case IFGT:
				case IFLE:
				case IFNULL:
				case IFNONNULL:
					popValue();
					break;

				case IF_ICMPEQ:
				case IF_ICMPNE:
				case IF_ICMPLT:
				case IF_ICMPGE:
				case IF_ICMPGT:
				case IF_ICMPLE:
				case IF_ACMPEQ:
				case IF_ACMPNE:
					popValue();
					popValue();
					break;

				case JSR:
					pushValue(OTHER);
					break;
			}
			addBranch(label);
		}
	}

	@Override
	public void visitLabel(final Label label) {
		mv.visitLabel(label);

		if (constructor && branches != null) {
			ArrayList frame = (ArrayList) branches.get(label);
			if (frame != null) {
				stackFrame = frame;
				branches.remove(label);
			}
		}
	}

	@Override
	public void visitLdcInsn(final Object cst) {
		mv.visitLdcInsn(cst);

		if (constructor) {
			pushValue(OTHER);
			if (cst instanceof Double || cst instanceof Long) {
				pushValue(OTHER);
			}
		}
	}

	@Override
	public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels) {
		mv.visitLookupSwitchInsn(dflt, keys, labels);

		if (constructor) {
			popValue();
			addBranches(dflt, labels);
		}
	}

	@Override
	public void visitMethodInsn(final int opcode, final String owner, final String name, final String desc) {
		mv.visitMethodInsn(opcode, owner, name, desc);

		if (constructor) {
			Type[] types = Type.getArgumentTypes(desc);
			for (int i = 0; i < types.length; i++) {
				popValue();
				if (types[i].getSize() == 2) {
					popValue();
				}
			}
			switch (opcode) {
				// case INVOKESTATIC:
				// break;

				case INVOKEINTERFACE:
				case INVOKEVIRTUAL:
					popValue(); // objectref
					break;

				case INVOKESPECIAL:
					Object type = popValue(); // objectref
					if (type == THIS && !superInitialized) {
						onMethodEnter();
						superInitialized = true;
						// once super has been initialized it is no longer
						// necessary to keep track of stack state
						constructor = false;
					}
					break;
			}

			Type returnType = Type.getReturnType(desc);
			if (returnType != Type.VOID_TYPE) {
				pushValue(OTHER);
				if (returnType.getSize() == 2) {
					pushValue(OTHER);
				}
			}
		}
	}

	@Override
	public void visitMultiANewArrayInsn(final String desc, final int dims) {
		mv.visitMultiANewArrayInsn(desc, dims);

		if (constructor) {
			for (int i = 0; i < dims; i++) {
				popValue();
			}
			pushValue(OTHER);
		}
	}

	@Override
	public void visitTableSwitchInsn(final int min, final int max, final Label dflt, final Label[] labels) {
		mv.visitTableSwitchInsn(min, max, dflt, labels);

		if (constructor) {
			popValue();
			addBranches(dflt, labels);
		}
	}

	@Override
	public void visitTypeInsn(final int opcode, final String name) {
		mv.visitTypeInsn(opcode, name);

		// ANEWARRAY, CHECKCAST or INSTANCEOF don't change stack
		if (constructor && opcode == NEW) {
			pushValue(OTHER);
		}
	}

	@Override
	public void visitVarInsn(final int opcode, final int var) {
		super.visitVarInsn(opcode, var);

		if (constructor) {
			switch (opcode) {
				case ILOAD:
				case FLOAD:
					pushValue(OTHER);
					break;
				case LLOAD:
				case DLOAD:
					pushValue(OTHER);
					pushValue(OTHER);
					break;
				case ALOAD:
					pushValue(var == 0 ? THIS : OTHER);
					break;
				case ASTORE:
				case ISTORE:
				case FSTORE:
					popValue();
					break;
				case LSTORE:
				case DSTORE:
					popValue();
					popValue();
					break;
			}
		}
	}

	/**
	 * Called at the beginning of the method or after super class class call in
	 * the constructor. <br>
	 * <br>
	 * 
	 * <i>Custom code can use or change all the local variables, but should not
	 * change state of the stack.</i>
	 */
	protected abstract void onMethodEnter();

	/**
	 * Called before explicit exit from the method using either return or throw.
	 * Top element on the stack contains the return value or exception instance.
	 * For example:
	 * 
	 * <pre>
	 *   public void onMethodExit(int opcode) {
	 *     if(opcode==RETURN) {
	 *         visitInsn(ACONST_NULL);
	 *     } else if(opcode==ARETURN || opcode==ATHROW) {
	 *         dup();
	 *     } else {
	 *         if(opcode==LRETURN || opcode==DRETURN) {
	 *             dup2();
	 *         } else {
	 *             dup();
	 *         }
	 *         box(Type.getReturnType(this.methodDesc));
	 *     }
	 *     visitIntInsn(SIPUSH, opcode);
	 *     visitMethodInsn(INVOKESTATIC, owner, &quot;onExit&quot;, &quot;(Ljava/lang/Object;I)V&quot;);
	 *   }
	 * 
	 *   // an actual call back method
	 *   public static void onExit(int opcode, Object param) {
	 *     ...
	 * </pre>
	 * 
	 * <br>
	 * <br>
	 * 
	 * <i>Custom code can use or change all the local variables, but should not
	 * change state of the stack.</i>
	 * 
	 * @param opcode
	 *            one of the RETURN, IRETURN, FRETURN, ARETURN, LRETURN, DRETURN
	 *            or ATHROW
	 * 
	 */
	protected abstract void onMethodExit(int opcode);

	private void addBranch(final Label label) {
		if (branches.containsKey(label)) {
			return;
		}
		ArrayList frame = new ArrayList();
		frame.addAll(stackFrame);
		branches.put(label, frame);
	}

	private void addBranches(final Label dflt, final Label[] labels) {
		addBranch(dflt);
		for (int i = 0; i < labels.length; i++) {
			addBranch(labels[i]);
		}
	}

	private Object peekValue() {
		return stackFrame.get(stackFrame.size() - 1);
	}

	private Object popValue() {
		return stackFrame.remove(stackFrame.size() - 1);
	}

	private void pushValue(final Object o) {
		stackFrame.add(o);
	}

	// TODO onException, onMethodCall

}
